/**
 *  This file is part of OhSnap.
 *
 * 	Copyright (c) 2009 Gabriel Mendonça and Marcelo Machado
 * 
 *  OhSnap is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  OhSnap is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with OhSnap.  If not, see <http://www.gnu.org/licenses/>.
 *  
 */
package br.mm.midi;

import java.io.File;
import java.io.IOException;
import java.util.List;

import javax.sound.midi.InvalidMidiDataException;
import javax.sound.midi.MetaEventListener;
import javax.sound.midi.MetaMessage;
import javax.sound.midi.MidiMessage;
import javax.sound.midi.MidiSystem;
import javax.sound.midi.MidiUnavailableException;
import javax.sound.midi.Receiver;
import javax.sound.midi.Sequencer;
import javax.sound.midi.ShortMessage;
import javax.sound.midi.Transmitter;

/**
 * A simple class to play MIDI files. It listens to the notes played and adds
 * them to a list, as <code>{@link MidiNote MidiNotes}</code>. If an
 * <code>{@link MidiEndListener}</code> was given, calls it's method
 * <code>midiEnd</code> when the music ends.
 * 
 * @author mendonça
 */
public class MidiPlayer implements MetaEventListener, Receiver {

	private File midiFile;
	private Sequencer sequencer;
	private Transmitter transmitter;
	private MidiEndListener endListener;
	private List<MidiNote> noteList;

	/**
	 * Construtor. Tries to load the file with the given <code>filePath</code>.
	 * 
	 * @param filePath
	 *            the path of the MIDI file to be load
	 * @throws Exception
	 */
	public MidiPlayer(String filePath) throws Exception {
		this(filePath, null);
	}

	/**
	 * Construtor. Tries to load the file with the given <code>filePath</code>.
	 * Registers the given <code>MidiEndListener</code>.
	 * 
	 * @param filePath
	 *            the path of the MIDI file to be load
	 * @param endListener
	 *            the <code>MidiEndListener</code> that will be used when the
	 *            music ends
	 * @throws Exception
	 */
	public MidiPlayer(String filePath, MidiEndListener endListener)
			throws Exception {
		this.midiFile = new File(filePath);
		this.endListener = endListener;

		try {
			loadSequencer();

			transmitter = sequencer.getTransmitter();
			transmitter.setReceiver(this);

			sequencer.open();

		} catch (MidiUnavailableException e) {
			String msg = "O dispositivo MIDI de saída não está disponível. "
					+ "Se estiver sendo utilizado por outra aplicação, feche-a.";
			handleException(e, msg);

		} catch (InvalidMidiDataException e) {
			String msg = "O caminho informado não aponta para "
					+ "um arquivo MIDI válido.";
			handleException(e, msg);

		} catch (IOException e) {
			String msg = "O arquivo " + filePath + " não foi encontrado.";
			handleException(e, msg);
		}
	}

	/**
	 * Starts to play the MIDI file. The notes played will be added to the list
	 * notes
	 * 
	 * @param noteList
	 *            the list in which the played notes will be added.
	 */
	public void play(List<MidiNote> notes) {
		noteList = notes;

		sequencer.start();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see javax.sound.midi.Receiver#send(javax.sound.midi.MidiMessage, long)
	 */
	/**
	 * If the sent message is a note on short message, instantiates a new
	 * MidiNote and adds it to the noteList.
	 */
	@Override
	public void send(MidiMessage message, long timeStamp) {
		if (message instanceof ShortMessage) {
			ShortMessage shortMessage = (ShortMessage) message;

			if (shortMessage.getCommand() == ShortMessage.NOTE_ON) {
				MidiNote note = new MidiNote(shortMessage);
				noteList.add(note);
			}
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * javax.sound.midi.MetaEventListener#meta(javax.sound.midi.MetaMessage)
	 */
	/**
	 * Listens for an "End-of-Stream" meta message.
	 */
	@Override
	public void meta(MetaMessage meta) {
		if (meta.getType() == 47) { // end of stream
			sequencer.stop();
			this.close();

			if (this.endListener != null) {
				endListener.midiEnd();
			}
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see javax.sound.midi.Receiver#close()
	 */
	/**
	 * Closes this player's
	 */
	@Override
	public void close() {
		if (transmitter != null)
			transmitter.close();

		if (sequencer != null)
			sequencer.close();
	}

	/**
	 * Instantiates and sets up a Sequencer.
	 * 
	 * @throws MidiUnavailableException
	 * @throws IOException
	 * @throws InvalidMidiDataException
	 */
	private void loadSequencer() throws MidiUnavailableException,
			InvalidMidiDataException, IOException {

		sequencer = MidiSystem.getSequencer();
		sequencer.setSequence(MidiSystem.getSequence(midiFile));
		sequencer.addMetaEventListener(this);
	}

	/**
	 * Handles an exception.
	 * 
	 * @param e
	 *            the Exception that will be handled.
	 * @param msg
	 *            the message that will be printed in the console.
	 * @throws Exception
	 */
	private void handleException(Exception e, String msg) throws Exception {
		System.out.println(msg);
		close();
		throw e;
	}

}
